﻿using System;
using System.Drawing;
using System.Windows.Forms;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;
using RPGOnline.Properties;

namespace RPGOnline.Rendering
{
	public class Renderer : IMainRenderer
	{
		private class RenderPass : IDisposable
		{
			private readonly RenderToSurface m_renderToSurface;
			private readonly Surface m_renderSurface;
			private readonly bool m_disposeRenderToSurface;

			public RenderPass(RenderToSurface renderToSurface, Surface renderSurface, bool disposeRenderToSurface = false)
			{
				m_renderToSurface = renderToSurface;
				m_renderSurface = renderSurface;
				m_disposeRenderToSurface = disposeRenderToSurface;
			}

			public void Dispose()
			{
				m_renderToSurface.EndScene(Filter.None);
				m_renderSurface.Dispose();

				if(m_disposeRenderToSurface)
					m_renderToSurface.Dispose();
			}
		}

		private class EffectRenderPass : IDisposable
		{
			private readonly IDisposable m_pass;
			private readonly Effect m_effect;
			private readonly Action m_completeAction;

			public EffectRenderPass(IDisposable pass, Effect effect, Action completeAction)
			{
				m_pass = pass;
				m_effect = effect;
				m_completeAction = completeAction;
			}

			public void Dispose()
			{
				m_effect.EndPass();
				m_effect.End();
				if(m_pass != null)
					m_pass.Dispose();
				m_completeAction();
			}
		}

		private readonly Control m_target;
		private Device m_device;
		private VertexBuffer m_quadBuffer;
		private SwapChain m_swapChain;
		private Surface m_depthStencilSurface;
		private RenderToSurface m_renderToSurface;
		private Texture m_renderTarget;
		private Effect m_occlusionMapEffect;
		private Effect m_currentShader;
		private Texture m_lightPlaceholderTexture;
		private VertexBuffer m_lightPlaceholderCircle;
		private const double ToRad = Math.PI / 180.0;

		public Renderer(Control target)
		{
			m_target = target;

			CreateDevice();
		}

		public Device Device
		{
			get
			{
				return m_device;
			}
		}

		public void Initialize()
		{
			CreateSwapChain();
		}

		private void CreateDevice()
		{
			PresentParameters presentParameters = GetPresentParameters();

			int adapterID = 0;
			DeviceType deviceType = DeviceType.Hardware;
			foreach (AdapterInformation adapter in Manager.Adapters)
			{
				if (adapter.Information.Description.Contains("PerfHUD"))
				{
					adapterID = adapter.Adapter;
					deviceType = DeviceType.Reference;
					break;
				}
			}


			m_device = new Device(adapterID, deviceType, m_target, CreateFlags.HardwareVertexProcessing, presentParameters);
			m_device.DeviceLost += OnDeviceLost;

			CreateSwapChain();
			InitializeBuffers();
		}

		private void CreateSwapChain()
		{
			PresentParameters presentParameters = GetPresentParameters();

			m_swapChain = new SwapChain(m_device, presentParameters);

			using (Surface backBuffer = m_swapChain.GetBackBuffer(0, BackBufferType.Mono))
			{
				m_depthStencilSurface = m_device.CreateDepthStencilSurface(backBuffer.Description.Width,
																		   backBuffer.Description.Height,
																		   DepthFormat.D24X8,
																		   MultiSampleType.None, 0, false);
			}

			m_device.DepthStencilSurface = m_depthStencilSurface;
			m_device.Viewport = new Viewport() { X = 0, Y = 0, Width = m_target.Width, Height = m_target.Height, MinZ = 0, MaxZ = 1 };

			m_renderToSurface = new RenderToSurface(m_device, m_target.Width, m_target.Height, Format.A8R8G8B8, true, DepthFormat.D24X8);
			m_renderTarget = new Texture(m_device, m_target.Width, m_target.Height, 1, Usage.RenderTarget, Format.A8R8G8B8, Pool.Default);

			m_device.Transform.Projection = Matrix.OrthoOffCenterLH(0, m_target.Width, m_target.Height, 0, 0, 1);
			m_device.Transform.World = Matrix.Identity;
			m_device.Transform.View = Matrix.Identity;
			m_device.RenderState.ZBufferEnable = false;
			m_device.RenderState.CullMode = Cull.None;

			m_device.RenderState.AlphaBlendEnable = true;
			m_device.RenderState.SourceBlend = Blend.SourceAlpha;
			m_device.RenderState.DestinationBlend = Blend.InvSourceAlpha;

			m_device.TextureState[0].ColorOperation = TextureOperation.SelectArg1;
			m_device.TextureState[0].ColorArgument1 = TextureArgument.TextureColor;
			m_device.TextureState[0].ColorArgument2 = TextureArgument.Diffuse;
			m_device.TextureState[0].TextureTransform = TextureTransform.Count3;

			m_device.TextureState[0].AlphaOperation = TextureOperation.Modulate;
			m_device.TextureState[0].AlphaArgument1 = TextureArgument.Diffuse;
			m_device.TextureState[0].AlphaArgument2 = TextureArgument.TextureColor;
		}

		private void OnDeviceLost(object sender, EventArgs e)
		{
			//m_backBuffer.Dispose();
			m_depthStencilSurface.Dispose();
			m_swapChain.Dispose();
			//m_quadBuffer.Dispose();
		}

		private PresentParameters GetPresentParameters()
		{
			PresentParameters presentParameters = new PresentParameters();
			presentParameters.AutoDepthStencilFormat = DepthFormat.D24X8;
			presentParameters.BackBufferCount = 1;
			presentParameters.BackBufferFormat = Format.A8R8G8B8;
			presentParameters.BackBufferWidth = m_target.Width;
			presentParameters.BackBufferHeight = m_target.Height;
			presentParameters.SwapEffect = SwapEffect.Discard;
			presentParameters.Windowed = true;
			presentParameters.DeviceWindow = m_target;
			presentParameters.PresentationInterval = PresentInterval.Immediate;
			return presentParameters;
		}

		private void InitializeBuffers()
		{
			m_quadBuffer = new VertexBuffer(typeof(CustomVertex.PositionColoredTextured), 4, m_device, Usage.WriteOnly, CustomVertex.PositionColoredTextured.Format, Pool.Managed);
			m_quadBuffer.SetData(new[]
			{
				new CustomVertex.PositionColoredTextured(0, 0, 0, Color.White.ToArgb(), 0, 0),
				new CustomVertex.PositionColoredTextured(0, 1, 0, Color.White.ToArgb(), 0, 1),
				new CustomVertex.PositionColoredTextured(1, 0, 0, Color.White.ToArgb(), 1, 0),
				new CustomVertex.PositionColoredTextured(1, 1, 0, Color.White.ToArgb(), 1, 1)
			}, 0, LockFlags.None);

			m_lightPlaceholderCircle = new VertexBuffer(typeof(CustomVertex.PositionColored), 361, m_device, Usage.WriteOnly, CustomVertex.PositionColored.Format, Pool.Managed);
			var vertices = new CustomVertex.PositionColored[361];
			for (int angle = 0; angle <= 360; angle++)
			{
				var x = (float)Math.Cos(angle*ToRad);
				var y = (float)Math.Sin(angle*ToRad);
				vertices[angle] = new CustomVertex.PositionColored(x, y, 1, Color.White.ToArgb());
			}
			m_lightPlaceholderCircle.SetData(vertices, 0, LockFlags.None);


			m_lightPlaceholderTexture = new Texture(m_device, Resources.lightbulb, Usage.None, Pool.Managed);

			string compilationErrors;
			m_occlusionMapEffect = LoadEffect(Resources.OcclusionMap, out compilationErrors);
			if (m_occlusionMapEffect == null)
				MessageBox.Show(compilationErrors);
		}

		public void DrawQuad(Matrix matrix)
		{
			DrawQuad(matrix, Matrix.Identity, null);
		}

		public void DrawQuad(Matrix matrix, Matrix textureMatrix, BaseTexture texture)
		{
			var oldTexture = m_device.GetTexture(0);
			var oldTextureMatrix = m_device.GetTransform(TransformType.Texture0);
			var oldWorld = m_device.GetTransform(TransformType.World);

			m_device.MultiplyTransform(TransformType.World, matrix);
			m_device.VertexFormat = m_quadBuffer.Description.VertexFormat;
			m_device.SetStreamSource(0, m_quadBuffer, 0);
			
			if (m_currentShader != null)
			{
				Matrix modelViewProjectionMatrix = m_device.Transform.World*m_device.Transform.View*m_device.Transform.Projection;
				Matrix shaderTextureMatrix = textureMatrix;
				var dx = shaderTextureMatrix.M31;
				var dy = shaderTextureMatrix.M32;
				shaderTextureMatrix.M31 = 0;
				shaderTextureMatrix.M32 = 0;
				shaderTextureMatrix.M41 = dx;
				shaderTextureMatrix.M42 = dy;

				m_currentShader.SetValue(m_currentShader.GetParameterBySemantic(null, "ModelViewProjection"), modelViewProjectionMatrix);
				m_currentShader.SetValue(m_currentShader.GetParameterBySemantic(null, "TextureMatrix"), shaderTextureMatrix);
				m_currentShader.SetValue(m_currentShader.GetParameterBySemantic(null, "Texture"), texture);
				

				m_currentShader.CommitChanges();
			}
			else
			{
				m_device.Transform.Texture0 = textureMatrix;
				m_device.SetTexture(0, texture);
			}

			m_device.DrawPrimitives(PrimitiveType.TriangleStrip, 0, 2);

			if (m_currentShader == null)
			{
				m_device.SetTransform(TransformType.Texture0, oldTextureMatrix);
				m_device.SetTexture(0, oldTexture);
			}

			m_device.SetTransform(TransformType.World, oldWorld);
		}

		public void DrawLineList(Matrix matrix, VertexBuffer vertexBuffer, int linesCount)
		{
			var oldWorld = m_device.GetTransform(TransformType.World);

			m_device.RenderState.AlphaBlendEnable = true;
			m_device.MultiplyTransform(TransformType.World, matrix);
			m_device.VertexFormat = vertexBuffer.Description.VertexFormat;
			m_device.SetStreamSource(0, vertexBuffer, 0);
			m_device.DrawPrimitives(PrimitiveType.LineList, 0, linesCount);

			m_device.SetTransform(TransformType.World, oldWorld);
		}

		public void DrawLineStrip(Matrix matrix, VertexBuffer vertexBuffer, int linesCount)
		{
			var oldWorld = m_device.GetTransform(TransformType.World);

			m_device.MultiplyTransform(TransformType.World, matrix);
			m_device.VertexFormat = vertexBuffer.Description.VertexFormat;
			m_device.SetStreamSource(0, vertexBuffer, 0);
			m_device.DrawPrimitives(PrimitiveType.LineStrip, 0, linesCount);

			m_device.SetTransform(TransformType.World, oldWorld);
		}

		public IDisposable BeginFirstPass()
		{
			Surface renderSurface = m_renderTarget.GetSurfaceLevel(0);
			m_renderToSurface.BeginScene(renderSurface);

			return new RenderPass(m_renderToSurface, renderSurface);
		}

		public IDisposable BeginSecondPass()
		{
			Surface renderSurface = m_swapChain.GetBackBuffer(0, BackBufferType.Mono);
			m_renderToSurface.BeginScene(renderSurface);

			return new RenderPass(m_renderToSurface, renderSurface);
		}

		public void Clear(Color color)
		{
			m_device.Clear(ClearFlags.Target, color, 1, 0);
		}

		public void Render()
		{
			m_swapChain.Present();
		}

		public Effect LoadEffect(string shaderCode, out string errors)
		{
			return Effect.FromString(m_device, shaderCode, null, "", ShaderFlags.None, null, out errors);
		}

		public BaseTexture GetFirstPassTexture()
		{
			return m_renderTarget;
		}

		public void DrawLightPlaceholder(Vector2 position, float radius, Color color)
		{
			Matrix lightMatrix = Matrix.Scaling(32, 32, 1) * Matrix.Translation(position.X - 16, position.Y - 16, 0);
			DrawQuad(lightMatrix, Matrix.Identity, m_lightPlaceholderTexture);

			lightMatrix = Matrix.Scaling(radius, radius, 1) * Matrix.Translation(position.X, position.Y, 0);
			DrawLineStrip(lightMatrix, m_lightPlaceholderCircle, 360);
		}

		public Texture CreateRenderTarget(int width, int height)
		{
			return new Texture(m_device, width, height, 1, Usage.RenderTarget, Format.A8R8G8B8, Pool.Default);
		}

		public IDisposable RenderTo(Texture renderTarget)
		{
			var surfaceDescription = renderTarget.GetLevelDescription(0);
			var renderSurface = renderTarget.GetSurfaceLevel(0);
			var renderToSurface = new RenderToSurface(m_device, surfaceDescription.Width, surfaceDescription.Height, surfaceDescription.Format, false, DepthFormat.Unknown);
			renderToSurface.BeginScene(renderSurface);

			return new RenderPass(renderToSurface, renderSurface, true);
		}

		public IDisposable BeginOcclusionMap(Texture occlusionMap, RectangleF bounds, float lightSize)
		{
			m_currentShader = m_occlusionMapEffect;
			
			var pass = RenderTo(occlusionMap);
			m_occlusionMapEffect.Technique = m_occlusionMapEffect.GetTechnique(0);
			m_occlusionMapEffect.Begin(FX.None);
			m_occlusionMapEffect.BeginPass(0);

			var oldViewport = m_device.Viewport;
			var oldProjection = m_device.Transform.Projection;

			m_device.Viewport = new Viewport() {X = 0, Y = 0, Width = (int) bounds.Width, Height = (int) bounds.Height};
			m_device.Transform.Projection = Matrix.OrthoOffCenterLH(bounds.Left, bounds.Right, bounds.Bottom, bounds.Top, 0, 1);
			m_currentShader.SetValue("resolution", lightSize);

			return new EffectRenderPass(pass, m_occlusionMapEffect, () =>
			{
				m_currentShader = null;
				m_device.Viewport = oldViewport;
				m_device.Transform.Projection = oldProjection;
			});
		}

		public IDisposable BeginShadowCast(Texture lightMap, float lightSize)
		{
			m_currentShader = m_occlusionMapEffect;

			var pass = RenderTo(lightMap);
			m_occlusionMapEffect.Technique = m_occlusionMapEffect.GetTechnique(1);
			m_occlusionMapEffect.Begin(FX.None);
			m_occlusionMapEffect.BeginPass(0);

			var oldViewport = m_device.Viewport;
			var oldProjection = m_device.Transform.Projection;

			m_device.Viewport = new Viewport() { X = 0, Y = 0, Width = 256, Height = 1 };
			m_device.Transform.Projection = Matrix.OrthoOffCenterLH(0, 256, 1, 0, 0, 1);
			m_currentShader.SetValue("resolution", lightSize);

			return new EffectRenderPass(pass, m_occlusionMapEffect, () =>
			{
				m_currentShader = null;
				m_device.Viewport = oldViewport;
				m_device.Transform.Projection = oldProjection;
			});
		}

		public IDisposable BeginLight(Texture lightMap, Color lightColor)
		{
			m_currentShader = m_occlusionMapEffect;

			m_occlusionMapEffect.Technique = m_occlusionMapEffect.GetTechnique(2);
			m_occlusionMapEffect.Begin(FX.None);
			m_occlusionMapEffect.BeginPass(0);

			m_occlusionMapEffect.SetValue("LightMap", lightMap);
			m_occlusionMapEffect.SetValue("lightColor", new ColorValue(lightColor.R, lightColor.G, lightColor.B, lightColor.A));

			return new EffectRenderPass(null, m_occlusionMapEffect, () =>
			{
				m_currentShader = null;
			});
		}

		public IDisposable BeginAmbientPass(Color ambientColor)
		{
			m_currentShader = m_occlusionMapEffect;

			m_occlusionMapEffect.Technique = m_occlusionMapEffect.GetTechnique(3);
			m_occlusionMapEffect.Begin(FX.None);
			m_occlusionMapEffect.BeginPass(0);

			m_occlusionMapEffect.SetValue("lightColor", new ColorValue(ambientColor.R, ambientColor.G, ambientColor.B, ambientColor.A));

			return new EffectRenderPass(null, m_occlusionMapEffect, () =>
			{
				m_currentShader = null;
			});
		}
	}
}